package engine.shader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.FloatBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL20.*;

public class Shader {

	private static HashMap<String, Integer> cache = new HashMap<String, Integer>();
	
	private String log;
	
	private int vertexShader;
	private int fragmentShader;
	
	private int program;
	
	private int vertexShaderLine;
	private int fragmentShaderLine;
	
	public Shader(String fileName, Map<Integer, String> attributes) {
		if(cache.containsKey(fileName)) {
			program = cache.get(fileName);
		} else {
			compileShader(fileName);
			
			//create the program
			program = glCreateProgram();
			
			//attach the shaders
			glAttachShader(program, vertexShader);
			glAttachShader(program, fragmentShader);
			
			if(attributes != null) {
				for(Entry<Integer, String> e: attributes.entrySet()) {
					glBindAttribLocation(program, e.getKey(), e.getValue());
				}
			}
			
			//link our program
			glLinkProgram(program);

			//grab our info log
			String infoLog = glGetProgramInfoLog(program, glGetProgrami(program, GL_INFO_LOG_LENGTH));
			
			//if some log exists, append it 
			if (infoLog!=null && infoLog.trim().length()!=0) {
				log += infoLog;
			}
			
			//if the link failed, throw some sort of exception
			if (glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE) {
				System.err.println("Failure in linking program. Error log:\n" + infoLog);
			}
			
			//detach and delete the shaders which are no longer needed
			glDetachShader(program, vertexShader);
			glDetachShader(program, fragmentShader);
			glDeleteShader(vertexShader);
			glDeleteShader(fragmentShader);
			
			cache.put(fileName, program);
		}
	}
	
	private synchronized void compileShader(String fileName) {
		StringBuffer vertexBuffer = new StringBuffer();
		StringBuffer fragmentBuffer = new StringBuffer();
		int currentLine = 0;
		
		StringBuffer current = null;
		
		File f = new File(fileName);
		
		InputStream inputStream = Shader.class.getClassLoader().getResourceAsStream(fileName);
		
		try{
			Scanner scanner = null;
			
			if(f.exists()) {
				scanner = new Scanner(new FileInputStream(fileName));
			} else {
				scanner = new Scanner(inputStream);
			}
			
			while(scanner.hasNext()) {
				String s = scanner.nextLine();
				if(s.trim().equals("#vertex shader")) {
					current = vertexBuffer;
					vertexShaderLine = currentLine + 1;
				} else if(s.trim().equals("#fragment shader")) {
					current = fragmentBuffer;
					fragmentShaderLine = currentLine + 1;
				} else if(current != null) {
					current.append(s + "\n");
				}
				
				currentLine++;
			}
			
			scanner.close();
			
		} catch(NullPointerException ex) {
			System.err.println("Could not find: " + fileName);

			return;
		} catch (FileNotFoundException e) {
			System.err.println("Could not find: " + fileName);

			return;
		}
		
		vertexShader = compileShader(fileName, vertexBuffer.toString(), GL_VERTEX_SHADER);
		fragmentShader = compileShader(fileName, fragmentBuffer.toString(), GL_FRAGMENT_SHADER);
	}
	
	private int compileShader(String fileName, String shaderSource, int type) {
		int shader = glCreateShader(type);
		
		glShaderSource(shader, shaderSource);
		
		glCompileShader(shader);
		
		String infoLog = glGetShaderInfoLog(shader, glGetShaderi(shader, GL_INFO_LOG_LENGTH));
		
		if(infoLog != null && infoLog.trim().length() != 0) {
			log += fileName + ": " + infoLog  + "\n";
		}
		
		if(glGetShaderi(shader, GL_COMPILE_STATUS) == GL_FALSE) {
			System.err.println("Failure in compiling " + fileName + ". Error log: \n" + replaceLineNumber(getLineNumber(type), infoLog));
			return 0;
		}
		
		return shader;
	}
	
	private String replaceLineNumber(int baseNumber, String message) {
		String[] split = message.split("\n");
		StringBuffer buffer = new StringBuffer();
		
		for(int i = 0; i < split.length; i++) {
			if(split[i].matches("[0-9]*:[0-9]*.*")) {
				String[] split2 = split[i].split(":");
				
				if(split2.length < 3) {
					System.out.println("There occured an error while correcting line of: " + split[i]);
					continue;
				}
				
				//add line number to first number in split2[1]
				int braket = split2[1].indexOf("(");
				
				if(braket >= 0) {
					int curNumber = Integer.parseInt(split2[1].substring(0, braket));
					split2[1] = (curNumber + baseNumber) + "(" + split2[1].substring(braket + 1);
				} else {
					int curNumber = Integer.parseInt(split2[1]);
					split2[1] = (curNumber + baseNumber) + "";
				}
				
				for(int j = 0; j < split2.length; j++) {
					buffer.append(split2[j]);
					
					if(j < split2.length - 1) {
						buffer.append(":");
					}
				}
				
				buffer.append("\n");
			} else if(split[i].matches("[0-9]*\\([0-9]\\).*")) {
				int firstBracket = split[i].indexOf("(");
				int lastBracket = split[i].indexOf(")");
				
				int curNumber = Integer.parseInt(split[i].substring(firstBracket + 1, lastBracket));

				buffer.append(split[i].substring(0, firstBracket + 1) + (curNumber + baseNumber) + split[i].substring(lastBracket, split[i].length()) + "\n");
			}
		}
		
		return buffer.toString();
	}
	
	private int getLineNumber(int type) {
		if(type == GL_VERTEX_SHADER) {
			return vertexShaderLine;
		} else if(type == GL_FRAGMENT_SHADER) {
			return fragmentShaderLine;
		}
		
		return 0;
	}
	
	public void enable() {
		glUseProgram(program);
	}
	
	public void disable() {
		glUseProgram(0);
	}
	
	public int getProgram() {
		return program;
	}
	
	public String getLog() {
		return log;
	}
	
	public void sendUniform1i(String name, int i0) {
		int location = glGetUniformLocation(program, name);
		glUniform1i(location, i0);
	}
	
	public void sendUniform1f(String name, float f0) {
		int location = glGetUniformLocation(program, name);
		glUniform1f(location, f0);
	}
	
	public void sendUniform4f(String name, float v0, float v1, float v2, float v3) {
		int location = glGetUniformLocation(program, name);
		glUniform4f(location, v0, v1, v2, v3);
	}
	
	public void sendUniform4f(String name, float[] values) {
		sendUniform4f(name, values[0], values[1], values[2], values[3]);
	}
	
	public void sendUniformMatrix4(String name, FloatBuffer buffer) {
		buffer.rewind();
		int location = glGetUniformLocation(program, name);
		glUniformMatrix4(location, false, buffer);
	}

	public void sendUniformMatrix3(String name, FloatBuffer buffer) {
		buffer.rewind();
		int location = glGetUniformLocation(program, name);
		glUniformMatrix3(location, false, buffer);
	}
}
